Add autonomous agents page under contract accounts - #2811
Conversation
Covers bounding a long-running agent's authority with two composable restriction layers (callee-side instruction-set design, key-side smart account policy) and three runtime pitfalls that don't show up until the agent is actually operating: simulation not verifying auth, stale cached authority in a long-running loop, and where LLM discretion should stop versus hard-coded limits. Sits alongside the existing Advanced contract account patterns page, which covers the design-time guardrail primitives this page composes for a specifically autonomous, unattended caller. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds guidance for safely bounding autonomous agents using contract restrictions and smart-account policies.
Changes:
- Documents layered authority restrictions.
- Covers simulation, revocation, and model-safety pitfalls.
- Links to relevant implementations and specifications.
Recommendation: NEEDS-CHANGES — Correct the overstated security and revocation claims, and use relative links for internal pages.
Suppressed comments (1)
docs/build/guides/contract-accounts/autonomous-agents.mdx:40
- This is an in-repository docs target and should be relative so it resolves in local and versioned previews.
- [OpenZeppelin Smart Accounts](https://developers.stellar.org/docs/tools/openzeppelin-contracts) — context rules, signers, and policies, audited by OpenZeppelin's security team, with formal verification by Certora in progress.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The composed-layers paragraph implied the key-wrapping (smart account) layer was operating alongside the callee-side restriction. It isn't yet: the deployed contract's context_rules are confirmed correct by reading them on-chain, but nothing has authorized a real signed transaction through it. Only the callee-side restriction (DeFindex's Invest/Unwind) has a track record of real signed calls. Correcting before that distinction gets cited as settled. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
docs/build/guides/contract-accounts/autonomous-agents.mdx:17
CallContract(vault)scopes the credential to an address, not a function, and DeFindex also exposes withdrawal and administrative entry points. The final sentence therefore overstates what the two described layers guarantee: the bound comes from assigning this account only theRebalanceManagerrole. State that dependency explicitly rather than claiming the whole contract can only return funds to itself.
Neither layer is sufficient alone for an unattended agent. Restrict only the callee, and a leaked key can still authorize anything the callee's own logic happens to allow — a swap at bad slippage, say, if the callee supports swaps at all. Restrict only the key, and you're trusting that the callee's logic never grows a footgun later. Composed, an agent's key can only reach one contract, and that contract can't move funds anywhere but back into itself.
docs/build/guides/contract-accounts/autonomous-agents.mdx:42
- Links from
docs/**to another page in this repository must be relative; the hard-coded published-site URL can also break preview/versioned builds. Point this link at the local MDX page.
- [OpenZeppelin Smart Accounts](https://developers.stellar.org/docs/tools/openzeppelin-contracts) — context rules, signers, and policies, audited by OpenZeppelin's security team, with formal verification by Certora in progress.
docs/build/guides/contract-accounts/autonomous-agents.mdx:29
- Revocation does not become ineffective when the process caches an earlier permission read: the contract evaluates its current on-chain role and authorization state for every submitted call, so a revoked agent's later transaction fails. Re-reading each cycle is still useful to stop constructing or submitting doomed transactions, but it is not what enforces revocation. Please correct this security behavior.
A human signer revokes access once, and the session ends there. A long-running agent process keeps looping regardless of what changed underneath it, so if it caches "am I still authorized" from the start of the process, a revocation made ten minutes into a multi-day run does nothing until the process restarts. Read the current role or permission state from chain at the top of every cycle, not once at boot.
Confirmed empirically (needsNonInvokerSigningBy(), raw simulation auth entries, both authMode values) that a Signer::Delegated's own signing requirement never surfaces via the SDK's standard discovery flow — full detail and repro in OpenZeppelin/stellar-contracts#863. Framed with the same precision as that issue: what's empirically confirmed vs. what's inferred about recording mode's internal behavior, not independently verified against the host implementation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
docs/build/guides/contract-accounts/autonomous-agents.mdx:17
- This overstates the callee-side guarantee. DeFindex investments send assets into strategy contracts, and swaps transfer input to a pair; the verified property is that proceeds cannot be directed to an arbitrary recipient, not that funds never move anywhere except back into the vault. Narrow the conclusion to the actual destination restriction.
Neither layer is sufficient alone for an unattended agent. Restrict only the callee, and a leaked key can still authorize anything the callee's own logic happens to allow — a swap at bad slippage, say, if the callee supports swaps at all. Restrict only the key, and you're trusting that the callee's logic never grows a footgun later. Composed, an agent's key can only reach one contract, and that contract can't move funds anywhere but back into itself.
docs/build/guides/contract-accounts/autonomous-agents.mdx:25
- This conflates two different operations:
enforceis asimulateTransactionauthorization mode, while an on-chain submission has no “enforcement mode.” The internal link also needs to be relative. Distinguish enforcement-mode simulation from actual transaction submission so readers can implement the deny-path test correctly.
[Recording-mode simulation](https://developers.stellar.org/docs/learn/fundamentals/contract-development/contract-interactions/transaction-simulation#recording-mode) records every `require_auth` call as successful, and "never emulates authorization failures... failing authorization is always an 'exceptional' situation." That's the right behavior for building a transaction to sign, and the wrong tool for testing that a restriction actually holds. A policy that looks correctly restrictive under simulation can still pass simulation for an action it should deny, because simulation was never checking the signature in the first place. To test the deny path, submit the transaction in enforcement mode and confirm it actually fails on-chain — don't infer it from a clean simulation.
docs/build/guides/contract-accounts/autonomous-agents.mdx:46
- Docs links to pages in this repository must be relative. Point this item at the local MDX source rather than the published-site URL.
- [OpenZeppelin Smart Accounts](https://developers.stellar.org/docs/tools/openzeppelin-contracts) — context rules, signers, and policies, audited by OpenZeppelin's security team, with formal verification by Certora in progress.
…al ones Verified each of Copilot's six review points against real source before responding, per the standing discipline: - DeFindex rebalance(): Copilot was right. The router's own source shows a swap's input leg transfers to the trading pool mid-execution, not only ever back to the vault. Narrowed the claim. - Nirium Invest/Unwind evidence: Copilot was wrong (or working from a stale source) — there are two of each, not one Invest and zero Unwind. Verified b7bf6d70... (Unwind) directly against Horizon before citing it; both hashes now linked explicitly instead of an unbacked claim. - Hardcoded published-site URLs (3 instances): fixed to relative links, verified with the repo's own scripts/check-relative-links.sh. - Caching/revocation: Copilot was right — __check_auth re-reads current rules from chain every check, so an on-chain revocation is effective regardless of what a process cached. Reframed: the real cost of caching is wasted cycles and delayed self-awareness, not a security bypass. - "Enforcement mode": fixed to name the real authMode values (enforce/ record/record_allow_nonroot) and be precise that a real submission, not a special mode, is what actually enforces auth. - AuthMode count/API usage: Copilot's core technical point (three modes, not two) is correct per the real CHANGELOG, even though the specific file path it cited doesn't exist in this repo. Checking this surfaced a real methodological gap in the linked issue's own multi-mode claim (OpenZeppelin/stellar-contracts#863) — softened this page's citation to what's independently confirmed, added a real on-chain settlement of the delegated-signer case as stronger evidence. pnpm exec prettier -c and scripts/check-relative-links.sh --range both pass clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
docs/build/guides/contract-accounts/autonomous-agents.mdx:25
- A deny path does not require an on-chain submission. This repository's Transaction Simulation page says enforcing mode executes
__check_authand is effectively equivalent to on-chain execution, while the signing guide explicitly recommends enforcing-mode simulation to catch authorization errors without paying fees. Instruct readers to provide the attempted transaction's auth entries/signatures and confirm that an enforcing-mode simulation fails; submission is optional.
[Recording-mode simulation](../../../learn/fundamentals/contract-development/contract-interactions/transaction-simulation.mdx#recording-mode) records every `require_auth` call as successful, and "never emulates authorization failures... failing authorization is always an 'exceptional' situation." That's the right behavior for building a transaction to sign, and the wrong tool for testing that a restriction actually holds. A policy that looks correctly restrictive under simulation can still pass simulation for an action it should deny, because simulation was never checking the signature in the first place. `simulateTransaction` takes an `authMode` of `enforce`, `record`, or `record_allow_nonroot` — to test the deny path, actually submit the transaction (a real submission always enforces authorization, `authMode` or not) and confirm it fails on-chain. Don't infer a deny path from a clean recording-mode simulation.
docs/build/guides/contract-accounts/autonomous-agents.mdx:33
- The chain cannot reject an attempt to sign: signing happens locally with the agent's private key and does not consult
__check_auth. Revocation is enforced when the transaction is simulated in enforcing mode or submitted, so saying the next transaction the agent “tries to sign” is rejected misstates the security boundary.
An on-chain revocation is effective the moment it lands, regardless of what any process has cached: `__check_auth` re-reads the account's current rules from chain on every check, so the very next transaction the agent tries to sign is rejected whether or not its own process noticed anything changed. What caching actually costs you is time and signal, not security. A long-running agent that only reads "am I still authorized" once at boot keeps looping anyway, spending cycles building and submitting transactions that fail on-chain one after another, instead of noticing the moment its authority changed and stopping or alerting. Read the current role or permission state from chain at the top of every cycle — not to keep the revocation itself effective, but so the agent finds out it's been cut off immediately, not after a string of failed submissions.
|
|
||
| **Restrict the key.** The complementary layer is scoping the caller's own credential, using the account-abstraction primitives already covered on this page and the [previous one](./advanced-patterns.mdx): a smart account whose only context rule is `CallContract(the one vault you want)`, with no default/catch-all rule installed. The absence of a fallback rule is what does the work — a context that isn't covered by an explicit rule has no rule that authorizes it. | ||
|
|
||
| Neither layer is sufficient alone for an unattended agent. Restrict only the callee, and a leaked key can still authorize anything the callee's own logic happens to allow — a swap at bad slippage, say, if the callee supports swaps at all. Restrict only the key, and you're trusting that the callee's logic never grows a footgun later. Composed, an agent's key can only reach one contract, and that contract can't move funds anywhere but back into itself. |
What
Adds a new page,
docs/build/guides/contract-accounts/autonomous-agents.mdx, sitting alongside the existing Advanced contract account patterns page. No existing files touched.Why
Advanced contract account patterns covers the individual guardrail primitives (spend limits, allow lists, policy signers, time rules, session keys, external policy contracts) — installed once, typically for a human session. An autonomous agent is a different caller: the same signing key stays live indefinitely and decides on its own when to act, so the restriction has to hold for as long as the agent keeps running, not just at setup.
This page covers two composable ways to bound an agent's authority (restricting the callee's own instruction set vs. restricting the caller's key via a smart account policy, and why you generally want both), plus three pitfalls that specifically only surface once the agent is actually operating rather than when the policy is installed:
require_authcalls as successful and never emulates authorization failures — testing a deny path requires actually submitting in enforcement mode, not reading a clean simulation.Closes with a short note on what a technically sound restriction does and doesn't resolve (the legal categorization question is separate from the technical one).
Notes
rebalance()instruction set is quoted from DeFindex's current public repo (verified directly, not from memory — an instruction set some notes elsewhere describe as having dropped swap support, which is not what the current code shows); the simulation behavior is quoted verbatim from this site's own Transaction Simulation page; the Certora line matches this site's own OpenZeppelin Contracts page wording exactly ("is being completed", not "completed" — no Certora report exists in OpenZeppelin's ownaudits/folder yet).sidebar_position: 45, between Advanced contract account patterns (40) and Contract account examples (50).pnpm check:mdxpasses clean against the real.prettierrc.js.Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com